/**
 * Functions and Types Regarding Random Number Generators
 */
#include <stdlib.h>
#include <math.h>
#include <time.h>
#include <pthread.h>
#include <semaphore.h>
#include "random.h"

typedef struct distribution_list distribution_list;
typedef struct rng_buffer_list rng_buffer_list;
typedef struct rng_list rng_list;

/**
 *Some Standard Prototypes we do not want to reveal to the rest of the application
 */
double generate_random_number(char *, void *);
double rand_const(void *);
double rand_norm(void *);
double rand_exp(void *);
double rand_uni(void *);
void *build_constant_distribution(cJSON *);
void *build_poisson_distribution(cJSON *);
void *build_uniform_distribution(cJSON *);
void *build_normal_distribution(cJSON *);
void add_distribution(char *, void*(*)(cJSON *), double(*)(void *));
distribution_list *get_distribution(char *);
/**
 * Mutual Exclusion Semaphores (Should probably change to pthread_mutex)
 */
static sem_t distributions_mutex;
static sem_t rng_buffers_mutex;
static sem_t rng_mutex;

typedef enum distribution_id distribution_id;

static int semaphores_set = 0;

enum distribution_id {
  constant,
  uniform,
  poisson,
  normal
};

struct distribution_list {
  distribution_list *next;
  char *key;
  void *(*setup)(cJSON *);
  double (*get_number)(void *);
};

struct rng_buffer_list {
  rng_buffer_list *next;
  pthread_t tid;
  struct drand48_data *buffer;
};

struct rng_list {
  rng_list *next;
  char *key;
  char *type;
  void *args;
};

static distribution_list *distribution_table[HASHSIZE];
static rng_buffer_list *rng_buffer_table[HASHSIZE];
static rng_list *rng_table[HASHSIZE];

void setup_random_environment()
{
  sem_init(&distributions_mutex, 0 , 1);
  sem_init(&rng_buffers_mutex, 0 , 1);
  sem_init(&rng_mutex, 0, 1);
  //Add support for all of our probability distributions
  add_distribution("constant", build_constant_distribution, rand_const);
  add_distribution("uniform", build_uniform_distribution, rand_uni);
  add_distribution("exponential", build_poisson_distribution, rand_exp);
  add_distribution("normal", build_normal_distribution, rand_norm);
  setup_rng("wg_rand", cJSON_Parse("{\"type\":\"uniform\", \"min\":0, \"max\":1}"));
}

hashed hash(char *s) {
  unsigned hashval;
  for (hashval = 0; *s != '\0'; s++)
    {
      hashval = *s + 31 * hashval;
    }
  return hashval % HASHSIZE;
}

hashed int_hash(unsigned long int x){
  return x % HASHSIZE;
} 

struct drand48_data * get_rng_buffer(pthread_t tid) {
  hashed target = int_hash((unsigned long int) tid);
  sem_wait(&rng_buffers_mutex);
  rng_buffer_list *list = rng_buffer_table[target];
  rng_buffer_list *p;
  for(p = list; p != NULL; p = list->next) 
    {
      if( ((unsigned long int) tid)  == (unsigned long int) p->tid)
	{
	  sem_post(&rng_buffers_mutex);
	  return p->buffer;
	}
    }
  sem_post(&rng_buffers_mutex);
  return NULL;
}

int setup_rng_buffer(pthread_t tid)
{
  hashed target = int_hash((unsigned long int) tid);
  sem_wait(&rng_buffers_mutex);
  rng_buffer_list *list = rng_buffer_table[target];
  rng_buffer_list *new = (rng_buffer_list *) malloc(sizeof(rng_buffer_list));
  struct drand48_data *buffer = (struct drand48_data*) malloc(sizeof(struct drand48_data));
  //Set up a seed
  time_t seconds;
  //Initialize the buffer
  srand48_r(time(&seconds), buffer);
  new->next = list; new->tid = tid; new->buffer = buffer;
  rng_buffer_table[target] = new;
  sem_post(&rng_buffers_mutex);
  return 1;
}

int remove_rng_buffer(pthread_t tid)
{
  hashed target = int_hash((unsigned long int) tid);
  sem_wait(&rng_buffers_mutex);
  rng_buffer_list *list = rng_buffer_table[target];
  rng_buffer_list *prev = NULL;
  for(list,prev; list != NULL; prev=list, list=list->next)
    {
      //Remove the buffer if necessary
      if(list->tid == tid)
        {
          if(prev != NULL) 
            {
              prev->next = list->next;
            } 
          else 
            {
              rng_buffer_table[target] = list->next;
            }
          free(list);
          sem_post(&rng_buffers_mutex);
          return 1;
        }
    }
  sem_post(&rng_buffers_mutex);
  return 0;
}

void *build_constant_distribution(cJSON *json) {
  cJSON *c = cJSON_GetObjectItem(json, "value"); 
  double constant_value = c->valuedouble;
  struct constant_distribution *d = malloc(sizeof(struct constant_distribution));
  d->constant = constant_value;
  return (void*) d;
}

void *build_poisson_distribution(cJSON *json) {
  cJSON *c = cJSON_GetObjectItem(json, "lambda");
  double lambda = c->valuedouble;
  struct poisson_distribution *d = malloc(sizeof(struct poisson_distribution));
  d->lambda = lambda;
  return (void *) d;
}

void *build_normal_distribution(cJSON *json) {
  cJSON *json_mean = cJSON_GetObjectItem(json, "mean");
  cJSON *json_std = cJSON_GetObjectItem(json, "std");
  double mean = json_mean->valuedouble;
  double std = json_mean->valuedouble;
  struct normal_distribution *d = malloc(sizeof(struct normal_distribution));
  d->mean = mean;
  d->std = std;
  return (void *) d;
}

void *build_uniform_distribution(cJSON *json) {
  cJSON *json_min = cJSON_GetObjectItem(json,"min");
  cJSON *json_max = cJSON_GetObjectItem(json,"max");
  double min = json_min->valuedouble;
  double max = json_max->valuedouble;
  struct uniform_distribution *d = malloc(sizeof(struct uniform_distribution));
  d->min = min;
  d->max = max;
  return (void *) d;
}

double get_random_number(char *key)
{
  hashed target = hash(key);
  sem_wait(&rng_mutex);
  rng_list *list = rng_table[target];
  rng_list *p;
  for(p = list; p != NULL; p = p->next)
    {
      if( strcmp(key, p->key) == 0)
	{
	  sem_post(&rng_mutex);
          return generate_random_number(p->type,p->args);
        }
    }
  sem_post(&rng_mutex);
  return 0;
}

int setup_rng(char *key, cJSON *json)
{
  hashed target = hash(key);
  cJSON *distribution_type_json = (cJSON_GetObjectItem(json, "type"));
  char *distribution_type = distribution_type_json->valuestring;
  sem_wait(&rng_mutex);
  if(distribution_type == NULL)
    {
      sem_post(&rng_mutex);
      return 0;
    }
  distribution_list *l = get_distribution(distribution_type);
  void *arguments = l->setup(json);
  rng_list *new = malloc(sizeof(rng_list));
  new->next = rng_table[target];
  new->type = distribution_type;
  new->key = key;
  new->args = arguments;
  rng_table[target] = new;
  sem_post(&rng_mutex);
  return 1;
}

double generate_random_number(char *distribution_type, void *arguments)
{
  hashed target = hash(distribution_type);
  sem_wait(&distributions_mutex);
  distribution_list *list = distribution_table[target];
  distribution_list *p = NULL;
  for(p = list; p != NULL; p = p->next)
    {
      if( strcmp(distribution_type, p->key) == 0 )
	{
	  sem_post(&distributions_mutex);
	  return p->get_number(arguments);
	}
    }
  sem_post(&distributions_mutex);
  return 0;
}

void add_distribution(char *distribution_type, void*(*setup_from_json)(cJSON *), double(*generate)(void *))
{
  hashed target = hash(distribution_type);
  sem_wait(&distributions_mutex);
  distribution_list *new = malloc(sizeof(distribution_list));
  new->next = distribution_table[target];
  new->key = distribution_type;
  new->setup = setup_from_json;
  new->get_number = generate;
  distribution_table[target] = new;
  sem_post(&distributions_mutex);
}

distribution_list *get_distribution(char *distribution_type) {
  hashed target = hash(distribution_type);
  sem_wait(&distributions_mutex);
  distribution_list *list = distribution_table[target];
  distribution_list *p;
  for(p = list; p != NULL; p = p->next)
    {
      if(strcmp(distribution_type, p->key) == 0)
        {
          sem_post(&distributions_mutex);
          return p;
        }
    }
  sem_post(&distributions_mutex);
  return NULL;
}

/**
 * Return a constant number
 */
double rand_const(void *arguments) {
  struct constant_distribution *d = (struct constant_distribution*)arguments;
  return d->constant;
}

/**
 * Retrieve an exponentially distributed random variable
 */
double rand_exp(void *arguments)
{
  struct poisson_distribution  *d = (struct poisson_distribution*)arguments;
  struct drand48_data *buffer = get_rng_buffer( pthread_self() );
  //u - uniformally distributed variable, e - exponentially distributed variable
  double u, e;
  //Fetch a the uniform value
  drand48_r(buffer, &u);
  //Substitute u into the equation for an exponentially distributed random variable.
  return (-log(1-u))/(d->lambda);
}

/**
 *Random variable with a standard normal distribution.
 */ 
double rand_z() 
{
  struct drand48_data *buffer = get_rng_buffer( pthread_self() );
  double sum = 0;
  int i;
  double mean = 0;
  double std = 0;
  //Sum up 100 random variables
  for(i = 0; i<100; i++) {
    double rand = 0;
    drand48_r(buffer, &rand);
    sum += rand;
  }
  //If n = 100, then mean is 100*1/2 and standard deviation is the square root of 100*(1-0)/12
  mean = 100.0*(0.5);
  std = sqrt(100.0/12.0); 	
	
  return (sum - mean)/std;
}

/**
 *Random with a normal distribution of mean U and standard deviation S
 */ 
double rand_norm(void *arguments)
{
  struct normal_distribution *d = (struct normal_distribution*)arguments;
  //z=x-u/s - Solve for X;	
  return rand_z()*d->std+d->mean;
}

/**
 * Uniformly distributed random number between min and max
 */
double rand_uni(void *arguments)
{
  struct uniform_distribution *d = (struct uniform_distribution*)arguments;
  struct drand48_data *buffer = get_rng_buffer( pthread_self() );
  double rand;
  drand48_r(buffer,&rand);
  //Check for the base case
  if(d->min == 0 && d->max == 1)
    {
      return rand;
    }
  return (d->min + (rand * (d->max - d->min)));
}
